<?php

declare(strict_types=1);

namespace app\service\sys;

use app\model\SysMenu;
use InvalidArgumentException;

class MenuService
{
    private const ALLOWED_FIELDS = [
        'parent_id',
        'type',
        'name',
        'identifier',
        'path',
        'component',
        'redirect',
        'meta',
        'permission_code',
        'client_type',
        'sort',
        'is_visible',
        'status',
    ];

    public function getMenuCollection(array $filters = []): array
    {
        $query = SysMenu::order([
            'sort' => 'asc',
            'id' => 'asc',
        ]);

        $includeDisabled = filter_var($filters['include_disabled'] ?? false, FILTER_VALIDATE_BOOLEAN);
        if (!$includeDisabled && !isset($filters['status'])) {
            $query->where('status', 1);
        }

        if (isset($filters['status'])) {
            $query->where('status', (int) $filters['status']);
        }

        if (isset($filters['is_visible'])) {
            $query->where('is_visible', (int) $filters['is_visible']);
        }

        if (!empty($filters['client_type'])) {
            $query->where('client_type', (string) $filters['client_type']);
        }

        $types = [];
        if (!empty($filters['type'])) {
            $types = (array) $filters['type'];
        } else {
            $includeButtons = filter_var($filters['include_buttons'] ?? false, FILTER_VALIDATE_BOOLEAN);
            if (!$includeButtons) {
                $types = ['menu'];
            }
        }

        if (!empty($types)) {
            $query->whereIn('type', $types);
        }

        if (!empty($filters['keyword'])) {
            $keyword = trim((string) $filters['keyword']);
            $query->where(static function ($subQuery) use ($keyword) {
                $subQuery->whereLike('name', '%' . $keyword . '%')
                    ->whereOr('identifier', 'like', '%' . $keyword . '%')
                    ->whereOr('path', 'like', '%' . $keyword . '%');
            });
        }

        $items = [];
        /** @var SysMenu $menu */
        foreach ($query->select() as $menu) {
            $items[] = $this->formatMenu($menu);
        }

        return $items;
    }

    public function getMenuTree(array $filters = []): array
    {
        $items = $this->getMenuCollection($filters);

        return $this->buildTree($items);
    }

    public function create(array $payload): SysMenu
    {
        $data = $this->preparePayload($payload, true);
        $this->assertValidParent($data['parent_id'] ?? 0);
        $this->ensureIdentifierUnique((string) ($data['identifier'] ?? ''));

        /** @var SysMenu $menu */
        $menu = SysMenu::create($data);

        return SysMenu::find((int) $menu->id);
    }

    public function update(SysMenu $menu, array $payload): SysMenu
    {
        $data = $this->preparePayload($payload, false);
        if ($data === []) {
            return $menu;
        }

        if (array_key_exists('identifier', $data) && $data['identifier'] !== $menu->identifier) {
            $this->ensureIdentifierUnique((string) $data['identifier'], (int) $menu->id);
        }

        if (array_key_exists('parent_id', $data)) {
            $this->assertValidParent((int) $data['parent_id'], (int) $menu->id);
        }

        $menu->save($data);

        return SysMenu::find((int) $menu->id);
    }

    public function delete(SysMenu $menu): bool
    {
        $childrenCount = $menu->children()->count();
        if ($childrenCount > 0) {
            throw new InvalidArgumentException('请先删除子菜单');
        }

        return (bool) $menu->delete();
    }

    public function buildTree(array $items): array
    {
        if ($items === []) {
            return [];
        }

        $indexed = [];
        foreach ($items as $item) {
            $item['children'] = $item['children'] ?? [];
            $indexed[$item['id']] = $item;
        }

        $tree = [];
        foreach ($indexed as $id => &$item) {
            $parentId = (int) ($item['parent_id'] ?? 0);
            if ($parentId > 0 && isset($indexed[$parentId])) {
                $indexed[$parentId]['children'][] = &$item;
            } else {
                $tree[] = &$item;
            }
        }
        unset($item);

        foreach ($indexed as &$item) {
            if ($item['children'] === []) {
                unset($item['children']);
            } else {
                $item['children'] = array_values($item['children']);
            }
        }
        unset($item);

        return array_values($tree);
    }

    public function formatMenu(SysMenu $menu): array
    {
        $data = $menu->toArray();
        $data['meta'] = $this->parseMeta($data['meta'] ?? null);

        return $data;
    }

    private function preparePayload(array $payload, bool $isCreate): array
    {
        $data = array_intersect_key($payload, array_flip(self::ALLOWED_FIELDS));

        if ($isCreate) {
            $data['parent_id'] = isset($data['parent_id']) ? (int) $data['parent_id'] : 0;
            $data['sort'] = isset($data['sort']) ? (int) $data['sort'] : 0;
            $data['is_visible'] = isset($data['is_visible']) ? (int) $data['is_visible'] : 1;
            $data['status'] = isset($data['status']) ? (int) $data['status'] : 1;
        } else {
            if (array_key_exists('parent_id', $data)) {
                $data['parent_id'] = (int) $data['parent_id'];
            }
            if (array_key_exists('sort', $data)) {
                $data['sort'] = (int) $data['sort'];
            }
            if (array_key_exists('is_visible', $data)) {
                $data['is_visible'] = (int) $data['is_visible'];
            }
            if (array_key_exists('status', $data)) {
                $data['status'] = (int) $data['status'];
            }
        }

        if (array_key_exists('meta', $data)) {
            $data['meta'] = $this->parseMeta($data['meta']);
        }

        return $data;
    }

    private function parseMeta(mixed $meta): ?array
    {
        if ($meta === null || $meta === '') {
            return null;
        }

        if (is_array($meta)) {
            return $meta;
        }

        if (is_string($meta)) {
            $decoded = json_decode($meta, true);
            if (json_last_error() !== JSON_ERROR_NONE) {
                throw new InvalidArgumentException('meta 字段必须是合法的 JSON');
            }

            return $decoded;
        }

        if ($meta instanceof \JsonSerializable) {
            return (array) $meta->jsonSerialize();
        }

        if (is_object($meta)) {
            return json_decode(json_encode($meta, JSON_UNESCAPED_UNICODE), true);
        }

        throw new InvalidArgumentException('meta 字段格式不正确');
    }

    private function ensureIdentifierUnique(string $identifier, ?int $excludeId = null): void
    {
        if ($identifier === '') {
            throw new InvalidArgumentException('标识不能为空');
        }

        $query = SysMenu::where('identifier', $identifier);
        if ($excludeId !== null) {
            $query->where('id', '<>', $excludeId);
        }

        if ($query->find()) {
            throw new InvalidArgumentException('标识已存在');
        }
    }

    private function assertValidParent(int $parentId, ?int $currentId = null): void
    {
        if ($parentId <= 0) {
            return;
        }

        if ($currentId !== null && $parentId === $currentId) {
            throw new InvalidArgumentException('父级不能指向自身');
        }

        /** @var SysMenu|null $parent */
        $parent = SysMenu::find($parentId);
        if (!$parent) {
            throw new InvalidArgumentException('父级菜单不存在');
        }

        if ($currentId === null) {
            return;
        }

        $ancestorId = (int) $parent->parent_id;
        while ($ancestorId > 0) {
            if ($ancestorId === $currentId) {
                throw new InvalidArgumentException('父级不能指向当前菜单的子节点');
            }
            $ancestor = SysMenu::find($ancestorId);
            if (!$ancestor) {
                break;
            }
            $ancestorId = (int) $ancestor->parent_id;
        }
    }
}
